package com.openess.bigsearch.engine.search;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;

import com.openess.bigsearch.engine.BPlusTree.BPlusTree;
import com.openess.bigsearch.engine.BPlusTree.StringBKCompare;
import com.openess.bigsearch.engine.BTree.util.Arrays;
import com.openess.bigsearch.engine.DAO.DBData;
import com.openess.bigsearch.engine.DAO.DBManager.Type;
import com.openess.bigsearch.engine.DAO.MysqlManager;
import com.openess.bigsearch.engine.Text.Text;
import com.openess.bigsearch.engine.Text.TextRaw;
import com.openess.bigsearch.engine.Text.TextRaw.Raw;
import com.openess.bigsearch.engine.document.Document;
import com.openess.bigsearch.engine.document.Field;
import com.openess.bigsearch.engine.index.Extent;
import com.openess.bigsearch.engine.index.Extent.ExtData;
import com.openess.bigsearch.engine.index.FrqPos;
import com.openess.bigsearch.engine.index.Nrm;
import com.openess.bigsearch.engine.utils.HashMap;
import com.openess.bigsearch.engine.utils.LinkedList;
import com.openess.bigsearch.engine.utils.Map;
import com.openess.bigsearch.engine.utils.PriorityQueue;

/**
 * @author TuTu
 * 
 */
public class IndexSearcher {

	public enum Sumy {
		YES, NO;
	}

	public enum Model {
		VSM, BM25, BM25F
	}

	/**
	 * 装载了范围表数据
	 */
	private Extent ext;
	/**
	 * 装载了域文本数据
	 */
	private static Text text;
	/**
	 * B+树
	 */
	private BPlusTree tree;
	/**
	 * 内保存有文档向量模长数据
	 */
	private Nrm nrm;

	/**
	 * 构造方法，根据路径，初始化索引B+树，以及分词解析器
	 * 
	 * @param indexPath
	 * @param version
	 * @throws IOException
	 */
	public IndexSearcher(String indexPath, int version) throws IOException {
		// 0是主索引
		tree = new BPlusTree(indexPath, new StringBKCompare(), version);
		ext = new Extent(indexPath, "r", version);
		text = new Text(indexPath, "r", version);
		nrm = new Nrm(indexPath, "r", version);
	}

	public IndexSearcher(String indexPath) throws IOException {
		this(indexPath, 0);
	}

	/**
	 * 在索引中查询字符串，可以是全文搜索，也可以是按域搜索，如果是按域进行搜索，那么返回的结果文档只可能是符合域范围的文档。
	 * 
	 * @param query
	 *            可以是一句话，也可以是一个单词，如果是一句话，那么将其拆分进行布尔与运算，取所有查询词的交集结果
	 * @param top
	 *            返回头几个文档
	 * @param sumy
	 *            搜索结果是否返回摘要 ，如果为NO，那么可以提高搜索效率
	 * @return 待查询字符串出错或者没有查询结果则返回null
	 * @throws IOException
	 */
	public TopResult search(Query query, int top, Sumy sumy, Model mod)
			throws IOException {
		if (query == null) {
			return null;
		}
		TopResult tr = new TopResult(query, top, sumy);
		if (query.isSingly()) {
			searchOne(query, 0, tr);
		} else {
			searchList(query, tr);
		}
		// 计算出分数最高的前top个文档
		tr.computeTop(mod);
		return tr;
	}

	/**
	 * 默认使用BM25F排序模型
	 * 。在索引中查询字符串，可以是全文搜索，也可以是按域搜索，如果是按域进行搜索，那么返回的结果文档只可能是符合域范围的文档。 * @param
	 * query
	 * 
	 * @param top
	 *            返回头几个文档
	 * @param sumy
	 *            搜索结果是否返回摘要 ，如果为NO，那么可以提高搜索效率
	 * @return 待查询字符串出错或者没有查询结果则返回null
	 * @throws IOException
	 */
	public TopResult search(Query query, int top, Sumy sumy) throws IOException {
		return search(query, top, sumy, Model.BM25F);
	}

	/**
	 * 传入的FrqPos[]代表一个查询词，因此每次调用本方法，都是只过滤了一个分词的倒排表，
	 * 对FrqPos[]进行域过滤，返回的多个FrqPos对象，
	 * 都是属于这个域内的，但每个FrqPos对象中的位置信息，不进行删除。域过滤过滤掉不符合的文档 ，但不会对最终排名分数有任何影响。
	 * 
	 * @param fieldName
	 * @param frpos
	 * @return FrqPos[]数组中的值，没有一个符合域要求的，那么返回空
	 * @throws IOException
	 */
	private FrqPos[] fieldFilter(String fieldName, FrqPos[] frpos)
			throws IOException {
		// 用来保存经过域过滤后的FrqPos
		LinkedList<FrqPos> list = new LinkedList<FrqPos>();
		ExtData[] array = ext.searchExtList(fieldName);
		for (FrqPos frpo : frpos) {
			Long docID = frpo.getId();
			int i = Arrays.binarySearch(array, ext.new ExtData(docID));
			if (i < 0)
				continue;
			ExtData extent = array[i];
			Integer[] pos = frpo.getPos();
			Integer[] ffrqList = frpo.getFfrqList();
			// 对属于域范围的分词位置进行过滤
			if (extent.contain(pos))
				list.add(new FrqPos(docID, pos, ffrqList));
		}
		if (list.isEmpty())
			return null;
		return list.toArray(new FrqPos());
	}

	/**
	 * 关键域的过滤，和普通的预过滤方式不同，直接查看倒排表即可完成过滤，不需要通过范围表。
	 * 传入的FrqPos[]代表一个查询词，因此每次调用本方法，都是只过滤了一个分词的倒排表，
	 * 对FrqPos[]进行域过滤，返回的多个FrqPos对象，
	 * 都是属于这个域内的，但每个FrqPos对象中的位置信息，不进行删除。域过滤过滤掉不符合的文档 ，但不会对最终排名分数有任何影响。
	 * 
	 * @param fieldIndex
	 *            需要过滤的关键域的下标位置
	 * @param frpos
	 * @return FrqPos[]数组中的值，没有一个符合域要求的，那么返回空
	 * @throws IOException
	 */
	private FrqPos[] mainFieldFilter(int fieldIndex, FrqPos[] frpos)
			throws IOException {
		// 用来保存经过域过滤后的FrqPos
		LinkedList<FrqPos> list = new LinkedList<FrqPos>();

		for (FrqPos frpo : frpos) {
			Long docID = frpo.getId();
			Integer[] pos = frpo.getPos();
			Integer[] ffrqList = frpo.getFfrqList();
			if (ffrqList[fieldIndex] > 0)
				list.add(new FrqPos(docID, pos, ffrqList));
		}
		if (list.isEmpty()) {
			return null;
		}
		return list.toArray(new FrqPos());
	}

	/**
	 * 专门用于有一组待搜索key的情况,内部利用了or布尔运算的原理,将每一个查询词的结果文档都进行保留，但并不是布尔检索模型，并没有将结果进行合并操作
	 * 。并将最终返回的结果进行排序，
	 * 排序利用向量空间模型。在多词查询中，很有可能同一篇文档中同时拥有多个查询词，这种情况并不会对现在的评分计算算法产生影响，因为利用
	 * term-at-a-time计算分子这种算法本身就是为了应对这种情况，会把每个查询词在同一篇文档中的分数进行相加。
	 * 
	 * @param query
	 *            查询的关键词
	 * @return 如果搜索没有搜索结果，或参数为空，那么操作不执行
	 * @throws IOException
	 */
	public void searchList(Query query, TopResult tr) throws IOException {
		if (query == null) {
			return;
		}
		for (int i = 0; i < query.length(); i++) {
			searchOne(query, i, tr);
		}
	}

	/**
	 * 未开发完成，未使用，用于两个待搜索key的与运算，内部判断是否需要使用跳转
	 * 
	 * @param key1
	 * @param key2
	 * @return
	 */
	public FrqPos[] searchTwoOFAnd(String key1, String key2) {
		// 跳跃指数，跳跃只发生在一边
		int k = 10;
		FrqPos[] list1 = tree.searchKey(key1);
		FrqPos[] list2 = tree.searchKey(key2);
		// 要保持list1是更长那个数组
		if (list2.length > list1.length) {
			FrqPos[] temp = list1;
			list1 = list2.clone();
			list2 = temp.clone();
		}
		// 如果两个数组长度差距很小或者最长数组的长度不够长，那么没必要使用跳转了，时间复杂度O(max)和O(min)差距不大
		if (Math.abs(list1.length - list2.length) < 2 * k
				|| Math.max(list1.length, list2.length) < 10 * k) {
			return andNoSkip(list1, list2);
		} else {
			return andSkip(list1, list2, k);
		}
	}

	/**
	 * 使用跳转进行优化的与运算，list1要求比list2更长
	 * 
	 * @param list1
	 * @param list2
	 * @param k
	 *            跳转指数
	 * @return
	 */
	private FrqPos[] andSkip(FrqPos[] list1, FrqPos[] list2, int k) {
		return null;
	}

	/**
	 * 不使用跳转进行优化的与运算，进行布尔运算的两个FrqPos进行了合并操作，将frq,pos数据进行了合并
	 * 
	 * @param list1
	 * @param list2
	 * @return 如果没有相同的文档，那么返回null
	 */
	public FrqPos[] andNoSkip(FrqPos[] list1, FrqPos[] list2) {
		if (list1 == null || list2 == null) {
			return null;
		}
		// 存储两个数组的共同数据
		FrqPos[] newList = new FrqPos[Math.min(list1.length, list2.length)];
		int i = 0;
		int j = 0;
		int n = 0;
		while (i != list1.length && j != list2.length) {
			if (list1[i].compareTo(list2[j]) > 0) {
				j++;
			} else if (list1[i].compareTo(list2[j]) < 0) {
				i++;
			} else {
				newList[n++] = andMerge(list1[i], list2[j]);
				i++;
				j++;
			}
		}
		// 如果没有相同的文档，那么返回null
		if (n == 0) {
			return null;
		}
		return java.util.Arrays.copyOfRange(newList, 0, n);
	}

	/**
	 * 不使用跳转进行优化的或运算，进行布尔运算的两个FrqPos进行了合并操作，将frq,pos数据进行了合并
	 * 
	 * @param list1
	 * @param list2
	 * @return
	 */
	public FrqPos[] orNoSkip(FrqPos[] list1, FrqPos[] list2) {
		if (list1 == null || list2 == null) {
			return null;
		}
		// 存储两个数组的全部数据
		FrqPos[] newList = new FrqPos[2 * Math.max(list1.length, list2.length)];
		int i = 0;
		int j = 0;
		int n = 0;
		while (i != list1.length || j != list2.length) {
			if (list1[i].compareTo(list2[j]) > 0) {
				newList[n++] = list2[j++];
			} else if (list1[i].compareTo(list2[j]) < 0) {
				newList[n++] = list2[i++];
			} else {
				newList[n++] = andMerge(list1[i], list2[j]);
				i++;
				j++;
			}
		}
		return null;
	}

	/**
	 * 将两个具有相同id的FrqPos对象进行与合并，(frq,pos)保存两个对象的pos，frq相乘，如果id不同，那么返回空
	 * 
	 * @param pos1
	 * @param pos2
	 * @return
	 */
	private FrqPos andMerge(FrqPos pos1, FrqPos pos2) {
		long id1 = pos1.getId();
		long id2 = pos2.getId();
		if (id1 != id2) {
			return null;
		}
		Integer[] list1 = pos1.getPos();
		Integer[] list2 = pos2.getPos();
		Integer[] list = Arrays.merge(list1, list2);

		return new FrqPos(id1, list, null);
	}

	/**
	 * 将两个具有相同id的FrqPos对象进行或合并，(frq,pos)保存两个对象的pos，frq相加，如果id不同，那么返回空
	 * 
	 * @param pos1
	 * @param pos2
	 * @return
	 */
	private FrqPos orMerge(FrqPos pos1, FrqPos pos2) {
		long id1 = pos1.getId();
		long id2 = pos2.getId();
		if (id1 != id2) {
			return null;
		}
		// 将同id但不同分词的位置数据合并
		Integer[] list1 = pos1.getPos();
		Integer[] list2 = pos2.getPos();
		Integer[] list = Arrays.merge(list1, list2);

		return new FrqPos(id1, list, null);
	}

	/**
	 * 专门用于搜索仅有一个待搜索key的情况，搜索结果保存在TopResult对象中
	 * 
	 * @param query
	 * @param index
	 *            查询query中的第几个词项
	 * @param tr
	 *            将搜索的结果FrqPos[]传给TopResult中，由其对数据进行处理
	 * @throws IOException
	 */
	private void searchOne(Query query, int index, TopResult tr)
			throws IOException {
		String key = query.getQueryList()[index];
		FrqPos[] frpos = tree.searchKey(key);
		if (frpos == null) {
			return;
		}
		// 如果按域搜索，那么将FrqPos进行域过滤
		if (query.isField()) {
			int findex = query.isMainField();
			// 关键域用不同的方法进行预过滤
			if (findex > 0) {

			} else
				frpos = fieldFilter(query.getFileName(), frpos);
		}
		if (frpos != null) {
			tr.add(frpos);
		}
	}

	/**
	 * 现在一个ds代表了一篇文档，内部保存文档的所有域数据，以及：1.每一篇文档对应查询语句的相似度得分。2.文档的最佳摘要
	 * 
	 * @author TuTu
	 * 
	 */
	public static class DocScore extends DBData implements Comparable<DocScore> {
		/**
		 * 文档的分数
		 */
		private Double score;
		/**
		 * 文章的最佳摘要
		 */
		private String sumy;
		private Document doc;

		public DocScore(long docID, Double score) {
			super();
			this.score = score;
			this.docID = docID + "";
		}

		/**
		 * Mysql才会用到
		 * 
		 * @param db
		 */
		public DocScore(DBData db) {
			super();
			this.docID = db.getDocID();
			this.content = db.getContent();
			this.description = db.getDescription();
			this.keywords = db.getKeywords();
			this.title = db.getTitle();
			this.url = db.getUrl();
		}

		@Override
		public int compareTo(DocScore o) {
			return score.compareTo(o.score);
		}

		public Double getScore() {
			return score;
		}

		public void setScore(Double score) {
			this.score = score;
		}

		public String getSumy() {
			return sumy;
		}

		public final void setSumy(String sumy) {
			this.sumy = sumy;
		}

		/**
		 * 拿到域文本
		 * 
		 * @return
		 * @throws IOException
		 */
		public Document doc() throws IOException {
			if (doc == null)
				loadDoc();
			return doc;
		}

		@Override
		public String getContent() {
			return doc.getField("content");
		}

		/**
		 * 加载域文本
		 * 
		 * @throws IOException
		 */
		private void loadDoc() throws IOException {
			doc = new Document();
			long id = Long.valueOf(docID);
			TextRaw tr = text.get(id);
			ArrayList<Raw> rlist = tr.getRlist();
			for (Raw r : rlist)
				doc.add(new Field(r.getFieldname(), r.getRaw()));
		}

		public String toString() {
			return "docID = " + docID + ", score = " + score + "\nSummy=\n"
					+ sumy + "\n";
		}
	}

	/**
	 * TopResult类完成的功能包括：1.计算每个文档的特征分数(包括查询的分数)。2.利用优先队列，拿到分数最高的几个文档
	 * 
	 * @author TuTu
	 * 
	 */
	public class TopResult {
		/**
		 * 优先队列，用来存储返回的结果中排名靠前的文档
		 */
		private PriorityQueue<DocScore> que;
		/**
		 * 通过add()方法传进来的FrqPos[]先放在链表中
		 */
		private LinkedList<FrqPos[]> list;
		/**
		 * 实现摘要的类，往其中传入id:(pos, weight)，用于计算文档摘要
		 */
		private Summary sumy;
		/**
		 * 判断是否摘要
		 */
		private Sumy s;
		/**
		 * 用于term-at-a-time中计算各个模型的部分分数
		 */
		private HashMap<Long, Double> set;
		private Query query;
		/**
		 * 总共搜素到多少篇文档
		 */
		private int totalHits;

		/**
		 * 构造方法
		 * 
		 * @param query
		 *            查询语句对象
		 * @param top
		 *            返回总结果的前几个
		 * @param s
		 *            是否进行摘要计算
		 */
		public TopResult(Query query, int top, Sumy s) {
			que = new PriorityQueue<DocScore>(top);
			this.query = query;
			set = new HashMap<Long, Double>();
			list = new LinkedList<FrqPos[]>();
			this.s = s;
			if (s == Sumy.YES)
				sumy = new Summary(ext);
		}

		/**
		 * 将FrqPos[]放入链表中，待用于计算文档特征值，并不需要知道对应这个FrqPos[]倒排表的分词具体是什么，
		 * 因为调用add方法的前提是查询词里，没有重复的词项，因此有多少个FrqPos[]就代表就多少个词项
		 * 
		 * @param term
		 * @param fpos
		 */
		public void add(FrqPos[] fpos) {
			list.add(fpos);
		}

		/**
		 * 得到分数最高的几个分档，放在优先队列中
		 * 
		 * @throws IOException
		 */
		public void computeTop(Model mod) throws IOException {
			switch (mod) {
			case VSM: // 计算文档特征值的分子
				process_SVM();
				break;
			case BM25:
				process_BM25();
				break;
			case BM25F:
				process_BM25F();
				break;
			}
			for (Map.Entry<Long, Double> e : set.entrySet()) {
				long docID = e.getKey();
				double result = e.getValue();
				// 向量空间模型，拿到的只是分子，必须继续处理一下
				if (mod == Model.VSM) {
					result = cos(docID, result);
				}
				que.enqueue(new DocScore(docID, result));
			}
		}

		/**
		 * 计算每篇文档对应查询语句的特征值的分子，并将结果保存于set集合中，形式：(docID, numerator)，公式：cos(Di,
		 * Q)，分子部分的计算利用term-at-a-time，如果list中包含相同的查询词对应的FrqPos[]，那么整个计算就会出错
		 * 
		 */
		private void process_SVM() {
			// 文档总数，用于计算逆文档频率
			int totalNum = nrm.getDocNum();
			// 用于计算查询词的模长
			double queryNorm = 0;
			// 每循环一次，计算一个查询词项，对应的所有文档
			for (FrqPos[] array : list) {
				// idf的值仅因分词不同而不同
				double idf = Arrays.IDF(array.length, totalNum);
				// 查询词项的idf和文档词项的idf值相同，且认为查询词项的tf=1
				double q = idf * Arrays.TF(1);
				queryNorm += Math.pow(q, 2);
				// 每一个array就代表一个查询词项的倒排表，而每一个FrqPos代表查询词项倒排表里的一个id对应的数据，因此下面的代码计算出了查询词包含的每一个文档对应的特征值的分子
				for (FrqPos fp : array) {
					double tf = Arrays.TF(fp.getFrq());
					if (s == Sumy.YES) {
						// 向Summary传入相关数据，计算最佳摘要
						sumy.add(fp.getId(), tf * idf, fp.getPos());
					}
					double diqj = tf * idf * q;
					// 计算每篇文档的向量特征值的分子部分，利用term-at-a-time算法
					set.put(fp.getId(), diqj);
				}
			}
			// 保存查询向量的模长
			query.setNorm(Math.sqrt(queryNorm));
			// 计算总共搜索到了多少篇文档，利用set取出了重复项
			totalHits = set.size();
		}

		/**
		 * 利用BM25模型进行排序，排序效果理论上比向量空间模型好很多，排序模型中考虑了：1.BIM二元性。2.文档长度。3.分词在文档中的词频。
		 * 4.分词在查询中的词频。和向量空间模型类似，都利用了term-at-a-document算法，遍历一遍所有分词所有的文档数据，
		 * 即可计算出所有文档对应的分数。
		 */
		private void process_BM25() {
			// 文档总数，用于计算逆文档频率
			int totalNum = nrm.getDocNum();
			// 总文档的平均长度
			int avLength = nrm.getTotalLength() / totalNum;
			// 每循环一次，计算一个查询词项对应的所有文档
			for (FrqPos[] array : list) {
				// 每个查询词对应的文档数
				int docNum = array.length;
				// idf的值仅因分词不同而不同
				double idf = Arrays.IDF(docNum, totalNum);
				// 每一个array就代表一个查询词项的倒排表，而每一个FrqPos代表查询词项倒排表里的一个id对应的数据，因此下面的代码计算出了一个查询词包含的每一个文档对应的BM25部分值
				for (FrqPos fp : array) {
					if (s == Sumy.YES) {
						double tf = Arrays.TF(fp.getFrq());
						// 向Summary传入相关数据，计算最佳摘要
						sumy.add(fp.getId(), tf * idf, fp.getPos());
					}
					// 文档字符长度
					int docLength = nrm.searchLength(fp.getId());
					// 长度比例,加double为了防止int转double出错
					double prop = ((double) docLength) / avLength;
					int dFrq = fp.getFrq();
					// 仅是部分分数，部分在于仅计算了一个查询分词对应一篇文档的分数，而不是所有查询词
					double score = Arrays.BM25(totalNum, docNum, docLength,
							prop, dFrq, 1);
					// 计算每篇文档的最终分数，利用term-at-a-time算法，将所有查询词的部分分数相加，得到一篇文档的最终分数
					set.put(fp.getId(), score);
				}
			}
			// 计算总共搜索到了多少篇文档，利用set取出了重复项
			totalHits = set.size();
		}

		/**
		 * @throws IOException
		 * 
		 */
		private void process_BM25F() throws IOException {
			// 文档总数，用于计算逆文档频率
			int totalNum = nrm.getDocNum();
			// 每循环一次，计算一个查询词项对应的所有文档
			for (FrqPos[] array : list) {
				// 每个查询词对应的文档数
				int docNum = array.length;
				// idf的值仅因分词不同而不同
				double idf = Arrays.IDF(docNum, totalNum);
				for (FrqPos fp : array) {
					if (fp.getId() == 1708) {
						System.out.println();
					}
					if (s == Sumy.YES) {
						double tf = Arrays.TF(fp.getFrq());
						// 向Summary传入相关数据，计算最佳摘要
						sumy.add(fp.getId(), tf * idf, fp.getPos());
					}
					double fscoreTotal = 0;
					Integer[] ffrqList = fp.getFfrqList();
					Integer[] fLength = ext.searchMain(fp.getId());
					// 仅计算关键域
					for (int i = 0; i < ffrqList.length; i++) {
						int ffrq = ffrqList[i];
						String field = Field.MAINFIELD[i];
						double fweight = Field.FWEIGHT[i];
						fscoreTotal += Arrays.fscore(fweight, ffrq, 0,
								fLength[i], ext.searchAvfLength(field));
					}
					// 仅是部分分数，部分在于仅计算了一个查询分词对应一篇文档的分数，而不是所有查询词
					double score = Arrays.MB25F(totalNum, docNum, fscoreTotal);
					// 计算每篇文档的最终分数，利用term-at-a-time算法，将所有查询词的部分分数相加，得到一篇文档的最终分数
					set.put(fp.getId(), score);
				}
			}
			// 计算总共搜索到了多少篇文档，利用set取出了重复项
			this.totalHits = set.size();
		}

		/**
		 * 利用传进来的分子值，计算最终的相似度值
		 * 
		 * @param docID
		 *            文档的ID
		 * @param numerator
		 *            分子的值
		 * @return
		 */
		private Double cos(long docID, double numerator) {
			double docNorm = nrm.searchNorm(docID);
			double qNorm = query.getNorm();
			return numerator / (docNorm * qNorm);
		}

		/**
		 * 最终计算完的前top个DocScore对象，按照从大到小的顺序排列
		 * 
		 * @return
		 * @throws IOException
		 */
		public final DocScore[] getDocScore() throws IOException {
			DocScore[] results = readTop(que);
			if (s == Sumy.YES) {
				for (DocScore ds : results)
					// 仅计算排名靠前文档的摘要
					ds.setSumy(sumy.compute(ds));
			}
			return results;
		}

		/**
		 * que中仅包含了(id, score)，本方法用于加载所有保存的域文本
		 * 
		 * @param que
		 *            此时的DocScore 只包含(id,score)
		 * @return
		 * @throws IOException
		 */
		private DocScore[] readTop(PriorityQueue<DocScore> que)
				throws IOException {
			DocScore[] dlist = new DocScore[que.size()];
			int i = 0;
			for (DocScore ds : que) {
				ds.doc();
				dlist[i++] = ds;
			}
			return dlist;
		}

		/**
		 * 总共搜素到多少篇文档
		 * 
		 * @return
		 */
		public final int getTotalHits() {
			return totalHits;
		}
	}
}
